﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows.Forms;

using EnvDTE;

using EnvDTE80;

using Extensibility;

using Microsoft.VisualStudio.CommandBars;

namespace CodepageMaintain
{
	/// <summary>The object for implementing an Add-in.</summary>
	/// <seealso class='IDTExtensibility2' />
	public class Codepager : IDTExtensibility2,
		IDTCommandTarget
	{
		#region Поля класса

		private DTE2 _dte;

		private AddIn _addIn;

		private readonly string _codepagerFullCommandName;

		private Dictionary<string, Encoding> _encodings;

		private string[] _encodingNames;

		private CommandBarControl _codepageControl;

		#endregion

		#region Константы

		private const string ShortName = "CodePager";

		private const string NoEncodingName = "(select new encoding)";

		#endregion

		#region Конструктор

		///<summary>Implements the constructor for the Add-in object. Place your
		///initialization code within this method.</summary>
		public Codepager()
		{
			_codepagerFullCommandName = GetFullCommandName(ShortName);
			this.BuildEncodingCache();
		}

		#endregion

		#region Перегруженные методы IDTExtensibility2

		///<summary>Implements the OnConnection method of the IDTExtensibility2 interface.
		///Receives notification that the Add-in is being loaded.</summary>
		///<param term='application'>Root object of the host application.</param>
		///<param term='connectMode'>Describes how the Add-in is being loaded.</param>
		///<param term='addInInst'>Object representing this Add-in.</param>
		///<seealso class='IDTExtensibility2' />
		public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst,
			ref Array custom)
		{
			_dte = (DTE2)application;
			_addIn = (AddIn)addInInst;

			switch (connectMode)
			{
				case ext_ConnectMode.ext_cm_UISetup:
					//CreateCodepager();
					break;

				case ext_ConnectMode.ext_cm_AfterStartup:
					InjectIntoSolutionWindow();
					break;
			}
		}

		///<summary>Implements the OnDisconnection method of the IDTExtensibility2
		///interface. Receives notification that the Add-in is being unloaded.</summary>
		///<param term='disconnectMode'>Describes how the Add-in is being unloaded.
		///</param>
		///<param term='custom'>Array of parameters that are host application specific.
		///</param>
		///<seealso class='IDTExtensibility2' />
		public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
		{
			try
			{
				if (_codepageControl != null)
				{
					_codepageControl.Delete(true);
				}
			}
			catch
			{
			}
		}

		///<summary>Implements the OnAddInsUpdate method of the IDTExtensibility2
		///interface. Receives notification when the collection of Add-ins has changed.
		///</summary>
		///<param term='custom'>Array of parameters that are host application specific.
		///</param>
		///<seealso class='IDTExtensibility2' />
		public void OnAddInsUpdate(ref Array custom)
		{
		}

		///<summary>Implements the OnStartupComplete method of the IDTExtensibility2
		///interface. Receives notification that the host application has completed
		///loading.</summary>
		///<param term='custom'>Array of parameters that are host application specific.
		///</param>
		///<seealso class='IDTExtensibility2' />
		public void OnStartupComplete(ref Array custom)
		{
			InjectIntoSolutionWindow();
		}

		///<summary>Implements the OnBeginShutdown method of the IDTExtensibility2
		///interface. Receives notification that the host application is being unloaded.
		///</summary>
		///<param term='custom'>Array of parameters that are host application specific.
		///</param>
		///<seealso class='IDTExtensibility2' />
		public void OnBeginShutdown(ref Array custom)
		{
			var a = 2;
		}

		#endregion

		#region Перегруженные методы IDTCommandTarget

		public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText,
			ref vsCommandStatus status, ref object commandText)
		{
			//if (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
			{
				//if (commandName == this._codepagerFullCommandName /*&& this.GetSelection() != null*/)
				{
					status = vsCommandStatus.vsCommandStatusEnabled | vsCommandStatus.vsCommandStatusSupported;
				}
			}
		}

		public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn,
			ref object varOut, ref bool handled)
		{
			handled = false;
			if (executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
			{
				if (commandName == this._codepagerFullCommandName)
				{
					DbgEnumerateProperties();

					if ((varIn != null) && varIn is string)
					{
						var encName = varIn as string;
						Encoding info;
						if (_encodings.TryGetValue(encName, out info))
						{
							SaveSelectionWithNewEncoding(GetSelection(), info);
						}
						handled = true;
					}
					else if (varIn == null)
					{
						varOut = GetActiveEncodingName();
						handled = true;
					}
				}
				else if (commandName == this._codepagerFullCommandName + "_1")
				{
					varOut = _encodingNames;
					handled = true;
				}
			}
		}

		#endregion

		#region Частные методы класса

		private void SaveSelectionWithNewEncoding(AutoCastCollection<UIHierarchyItem> items,
			Encoding newEncoding)
		{
			foreach (var item in items)
			{
				if (item.Object is Solution)
				{
					var solution = item.Object as Solution;
					SaveSelectionWithNewEncoding_Solution(solution, newEncoding);
				}
				else if (item.Object is Project)
				{
					var project = item.Object as Project;
					SaveSelectionWithNewEncoding_Project(project, newEncoding);
				}
				else if (item.Object is ProjectItem)
				{
					var projectItem = item.Object as ProjectItem;
					SaveSelectionWithNewEncoding_ProjectItem(projectItem, newEncoding);
				}
			}
		}

		private void SaveSelectionWithNewEncoding_Solution(Solution solution, Encoding newEncoding)
		{
			foreach (Project project in solution.Projects)
			{
				SaveSelectionWithNewEncoding_Project(project, newEncoding);
			}
		}

		private void SaveSelectionWithNewEncoding_Project(Project project, Encoding newEncoding)
		{
			foreach (ProjectItem projectItem in project.ProjectItems)
			{
				SaveSelectionWithNewEncoding_ProjectItem(projectItem, newEncoding);
			}
		}

		private void SaveSelectionWithNewEncoding_ProjectItem(ProjectItem item, Encoding newEncoding)
		{
			switch (item.Kind)
			{
				case Constants.vsProjectItemKindPhysicalFolder:
					foreach (ProjectItem subItem in item.ProjectItems)
					{
						SaveSelectionWithNewEncoding_ProjectItem(subItem, newEncoding);
					}
					break;

				case Constants.vsProjectItemKindPhysicalFile:
					SaveSelectionWithNewEncodingCore(item, newEncoding);
					break;

				default:
					//log?
					break;
			}
		}

		private void SaveSelectionWithNewEncodingCore(ProjectItem item, Encoding newEncoding)
		{
			var path = item.Document.FullName;
			var binFile = File.ReadAllBytes(path);

			var filter = new EncodingFilter(_encodings.Values);
			var iterator = filter.Reset();
			var i = 0;
			while(iterator.Count > 1)
			{
				try
				{
					iterator.Append(binFile[i++]);
				}
				catch (Exception)
				{
					return;
				}
			}

			var srcEncoding = iterator.Result;

			var newBinFile = Encoding.Convert(srcEncoding, newEncoding, binFile);

			File.WriteAllBytes(path, newBinFile);
		}

		private void CreateToolWindow()
		{
			try
			{
				object control = null;
				var assemblyName = Assembly.GetExecutingAssembly().Location;
				var typeName = typeof(DbgOutput).FullName;
				var guid = string.Format("{{{0}}}", this._windowGuid).ToUpperInvariant();
				this._window = ((Windows2)this._dte.Windows).CreateToolWindow2(this._addIn, assemblyName,
					typeName, DbgOutput.DefaultCaption, guid, ref control);
				this._window.Visible = true;

				if (control is DbgOutput)
				{
					this._control = (DbgOutput)control;
				}
				else
				{
					throw new Exception("Unknown control returned");
				}
			}
			catch (Exception e)
			{
				MessageBox.Show(e.Message);
			}
		}

		private void DbgEnumerateProperties()
		{
			if (this._control == null)
			{
				return;
			}

			var items = this.GetSelection();
			UIHierarchyItem item;
			if (items.Count > 0)
			{
				item = items[0];
			}
			else
			{
				return;
			}

			var obj = item.Object;
			Properties props = null;
			if (obj is Solution)
			{
				props = ((Solution)obj).Properties;
			}
			else if (obj is Project)
			{
				props = ((Project)obj).Properties;
			}
			else if (obj is ProjectItem)
			{
				props = ((ProjectItem)obj).Properties;
			}

			var holder = props == null ? null : new DtePropertiesObject(props, obj);
			if (this._control != null)
			{
				this._control.propertyGrid.SelectedObject = null;
				try
				{
					this._control.propertyGrid.SelectedObject = holder == null
						? null : new DtePropertiesObjectTD(null, holder);
				}
				catch (Exception e)
				{
				}
			}
		}

		private static string GetActiveEncodingName()
		{
			var result = NoEncodingName;
			return result;
		}

		private void BuildEncodingCache()
		{
			try
			{
				if (this._encodings != null)
				{
					this._encodings.Clear();
				}

				var codes = new ArrayList { 1251, "65000", 65001, "utf-16", "utf-32" };
				this._encodings = new Dictionary<string, Encoding>(codes.Count);
				foreach (var code in codes)
				{
					try
					{
						Encoding enc;
						if (code is Int32)
						{
							enc = Encoding.GetEncoding((Int32)code);
						}
						else if (code is string)
						{
							int cp;
							enc = Int32.TryParse((string)code, out cp) ? Encoding.GetEncoding(cp) : Encoding.GetEncoding((string)code);
						}
						else
						{
							//log: not found
							continue;
						}

						this._encodings.Add(string.Format("{0} - cp{1}", enc.BodyName, enc.CodePage), enc);
					}
					catch (ArgumentException)
					{
						//log: not found
					}
				}

				var names = new List<string>(this._encodings.Count);
				foreach (var name in this._encodings.Keys)
				{
					names.Add(name);
				}

				//names.Sort((x, y) => (x.Contains("1251") || x.Contains("65001")) && x != y ? 1 : string.Compare(x, y));

				var top = names.FindAll(TopCodepagePredicate);
				names.RemoveAll(TopCodepagePredicate);
				names.InsertRange(0, top);

				this._encodingNames = names.ToArray();
			}
			catch (Exception e)
			{
				_encodingNames = new[] { string.Format("(internal error : {0})", e.Message) };
			}
		}

		private static bool TopCodepagePredicate(string obj)
		{
			return obj.Contains("1251") || obj.Contains("65001");
		}

		private string GetFullCommandName(string shortName)
		{
			return string.Format("{0}.{1}", GetType().FullName, shortName);
		}

		private void InjectIntoSolutionWindow()
		{
			var solutionExplorer = _dte.ToolWindows.SolutionExplorer;
			var window = (Window2)solutionExplorer.Parent;
			var commandBars = (CommandBars)window.CommandBars;
			var owner = commandBars["Explorer"];

			//commandBars = (CommandBars)_dte.CommandBars;
			//owner = commandBars["Standard"];

			var command = FindCommand(this._codepagerFullCommandName) ?? CreateCodepager();
			var protection = owner.Protection;
			if ((owner.Protection & MsoBarProtection.msoBarNoCustomize) != 0)
			{
				owner.Protection -= MsoBarProtection.msoBarNoCustomize;
			}

			try
			{
				this._codepageControl = (CommandBarControl)command.AddControl(owner, 0x17);
				this._codepageControl.BeginGroup = true;
				this._codepageControl.Width = 0x100;
			}
			finally
			{
				owner.Protection = protection;
			}

			CreateToolWindow();
		}

		private DbgOutput _control;

		private Window _window;

		private readonly Guid _windowGuid = new Guid("3CE28253-0440-4582-A04E-30F292A9C50D");

		private Command FindCommand(string fullCommandName)
		{
			foreach (Command command in this._dte.Commands)
			{
				if (command.Name == fullCommandName)
				{
					return command;
				}
			}

			return null;
		}

		private Command CreateCodepager()
		{
			var commands = (Commands2)_dte.Commands;
			var contextUIGuids = new object[0];
			return commands.AddNamedCommand2(_addIn, ShortName, ShortName,
				"Changes codepage of selected item(s) and subitem(s)", true, null, ref contextUIGuids,
				(int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled,
				(int)vsCommandStyle.vsCommandStylePictAndText,
				vsCommandControlType.vsCommandControlTypeDropDownCombo);
		}

		private AutoCastCollection<UIHierarchyItem> GetSelection()
		{
			return new AutoCastCollection<UIHierarchyItem>(_dte.ToolWindows.SolutionExplorer.SelectedItems);
		}

		#endregion
	}

	internal class EncodingFilter
	{
		internal class FilterIterator
		{
			private readonly EncodingFilter _parent;

			private readonly Encoding _default = Encoding.Default;

			private List<Encoding> _i;

			private int _pos = 0;

			public FilterIterator(EncodingFilter parent)
			{
				this._parent = parent;
				_i = new List<Encoding>(_parent._list);
				if(!_i.Contains(_default))
				{
					_i.Add(_default);
				}
			}

			public void Append(byte b)
			{
				if(Count <= 1)
				{
					return;
				}
				var fl = new List<Encoding>(_i.Count);
				foreach (var encoding in _i)
				{
					var p = encoding.GetPreamble();
					if(p.Length < _pos && p[_pos] == b)
					{
						fl.Add(encoding);
					}
				}

				_pos++;
				_i = fl;
			}

			public int Count
			{
				get { return _i.Count; }
			}

			public Encoding Result
			{
				get
				{
					return _i.Count == 0 ? _default : _i[0];
				}
			}
		}

		private readonly ReadOnlyCollection<Encoding> _list;

		public EncodingFilter(IEnumerable<Encoding> values)
		{
			this._list = new List<Encoding>(values).AsReadOnly();
		}

		public FilterIterator Reset()
		{
			return new FilterIterator(this);
		}

	}
}